/* Copyright Thanh-Trung Pham, JAIST, 2013 */
package jp.ac.jaist.simulation;

import com.google.common.collect.Sets;
import jp.ac.jaist.*;
import jp.ac.jaist.BranchingStructureType.IfPart;
import jp.ac.jaist.MultiTryCatchStructureType.MultiTryCatchPart;
import jp.ac.jaist.input.*;
import org.apache.commons.collections.SetUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.*;

/**
 * @author Thanh-Trung Pham
 */
public class RMPIReliabilitySimulator extends Thread {

    private static final String RMPI_COMMA = ",";
    private static final String RMPI_ALL = "ALL";
    private static final String RMPI_NONE = "NONE";
    private UsageProfilePartType m_oUsageProfilePart;
    private Set<Set<String>> m_oOrderedAFS;
    private Set<Set<String>> m_oOrderedAIOS;
    private boolean m_isStopped;
    private int m_iNumberOfRuns;

    private RMPIInputModeler m_oInputModel;
    private RMPIReliabilitySimulationStatistics m_oSimulationStatistics;
    private Set<RMPIReliabilitySimulationListener> m_oListeners;

    /**
     * Create a new instance.
     * @param inputModel the input model.
     * @param numberOfRuns the number of runs.
     * @return the newly created instance.
     */
    public static RMPIReliabilitySimulator newInstance(RMPIInputModeler inputModel, int numberOfRuns){
        return new RMPIReliabilitySimulator(inputModel, numberOfRuns);
    }

    /**
     * The constructor.
     * @param inputModel the input model.
     * @param numberOfRuns the number of runs.
     */
    private RMPIReliabilitySimulator(RMPIInputModeler inputModel, int numberOfRuns) {
        m_iNumberOfRuns = numberOfRuns;
        m_oInputModel = inputModel;

        m_oOrderedAFS = SetUtils.orderedSet(m_oInputModel.getAFS());
        m_oOrderedAIOS = SetUtils.orderedSet(m_oInputModel.getAIOS());
        m_oListeners = new HashSet<RMPIReliabilitySimulationListener>();
        m_oSimulationStatistics = new RMPIReliabilitySimulationStatistics();
    }

    /**
     * Add a listener.
     *
     * @param listener the listener to add.
     */
    public void addListener(RMPIReliabilitySimulationListener listener) {
        m_oListeners.add(listener);
    }

    /**
     * Remove a listener.
     *
     * @param listener the listener to remove.
     */
    public void removeListener(RMPIReliabilitySimulationListener listener) {
        m_oListeners.remove(listener);
    }

    /**
     * Set the stopped state.
     *
     * @param stopped the stopped state.
     */
    public void setStopped(boolean stopped) {
        this.m_isStopped = stopped;
    }

    /**
     * Run a simulation for the input model.
     *
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation() throws RMPIReliabilitySimulationException {
        UsageProfileType oUsageProfile = m_oInputModel.getUsageProfile();
        UsageProfilePartType[] oUsageProfileParts = oUsageProfile.getUsageProfilePartArray();

        double dTotalProbability = 0.0;
        double dRandomProbability = Math.random();
        for (int i = 0; i < oUsageProfileParts.length; i++) {
            dTotalProbability += oUsageProfileParts[i].getProbability();
            if (dRandomProbability < dTotalProbability) {
                m_oUsageProfilePart = oUsageProfileParts[i];

                UserInterfaceType oUserInterface = m_oInputModel.getUserInterfaceByName(m_oUsageProfilePart.getForUserInterface());
                String strComponentInstanceName = oUserInterface.getFromService().getFromComponentInstance();
                String strProvidedServiceName = oUserInterface.getFromService().getFromProvidedService();
                return runSimulation(strComponentInstanceName, strProvidedServiceName, RMPIInputModeler.RMPI_F0_AS_SET);
            }
        }

        //never reach
        return null;
    }

    /**
     * Run a simulation for a provided service implementation with an input.
     *
     * @param componentInstanceName the current component instance name.
     * @param providedServiceName   a provide service name.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, String providedServiceName, Set<String> input) throws RMPIReliabilitySimulationException {
        ComponentInstanceType oComponentInstance = m_oInputModel.getComponentInstance(componentInstanceName);
        ServiceImplementationType oServiceImplementation = m_oInputModel.getServiceImplementation(oComponentInstance.getForComponent(), providedServiceName);

        return runSimulation(componentInstanceName, oServiceImplementation, input);
    }

    /**
     * Run a simulation for a call propagation with an input.
     *
     * @param componentInstanceName the current component instance.
     * @param callPropagation       the call propagation.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, CallPropagationType callPropagation, Set<String> input) throws RMPIReliabilitySimulationException {
        Set<String> oReturnSet = null;

        if (callPropagation.isSetInternalActivity()) {
            oReturnSet = runSimulation(callPropagation.getInternalActivity(), input);
        } else if (callPropagation.isSetCallingActivity()) {
            oReturnSet = runSimulation(componentInstanceName, callPropagation.getCallingActivity(), input);
        } else if (callPropagation.isSetBranchingStructure()) {
            oReturnSet = runSimulation(componentInstanceName, callPropagation.getBranchingStructure(), input);
        } else if (callPropagation.isSetLoopingStructure()) {
            oReturnSet = runSimulation(componentInstanceName, callPropagation.getLoopingStructure(), input);
        } else if (callPropagation.isSetSequentialStructure()) {
            oReturnSet = runSimulation(componentInstanceName, callPropagation.getSequentialStructure(), input);
        } else if (callPropagation.isSetParallelStructure()) {
            oReturnSet = runSimulation(componentInstanceName, callPropagation.getParallelStructure(), input);
        } else if (callPropagation.isSetRetryStructure()) {
            oReturnSet = runSimulation(componentInstanceName, callPropagation.getRetryStructure(), input);
        } else if (callPropagation.isSetMultiTryCatchStructure()) {
            oReturnSet = runSimulation(componentInstanceName, callPropagation.getMultiTryCatchStructure(), input);
        } else if(callPropagation.isSetMVPStructure()){
            oReturnSet = runSimulation(componentInstanceName, callPropagation.getMVPStructure(), input);
        }

        return oReturnSet;
    }

    /**
     * Run a simulation for a MVP structure with an input.
     *
     * @param componentInstanceName the current component instance name.
     * @param mvpStructure    a MVP structure.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, MVPStructureType mvpStructure, Set<String> input) throws RMPIReliabilitySimulationException {
        int iMaxTI = mvpStructure.getMaxToleratedIncompletion();
        int iMinMS = mvpStructure.getMinMajoritySize();
        int iNumberOfMVPParts = mvpStructure.sizeOfMVPPartArray();

        List<Set<String>> oListOfOutputs = new ArrayList<Set<String>>();


        RMPIErrorPropertyVectorWrapper oErrorPropertyVectorWrapper = null;
        MVPStructureType.MVPPart[] oMVPParts = mvpStructure.getMVPPartArray();

        for(int i=0; i<oMVPParts.length; i++){
            if(i < mvpStructure.sizeOfErrorPropertyVectorArray()){
                oErrorPropertyVectorWrapper = new RMPIErrorPropertyVectorWrapper(mvpStructure.getErrorPropertyVectorArray(i));
            }

            try {
                Set<String> oOutput = runSimulation(componentInstanceName, oMVPParts[i], input);
                if(Math.random() < oErrorPropertyVectorWrapper.getProbability(oOutput)){
                    //do nothing
                }else{
                    oListOfOutputs.add(oOutput);
                }
            } catch (RMPIReliabilitySimulationException oSignaledFailure) {
                //do nothing
            }
        }

        for(MVPStructureType.MVPPart oMVPPart : mvpStructure.getMVPPartArray())  {

        }

        int iX = Collections.frequency(oListOfOutputs, RMPIInputModeler.RMPI_F0_AS_SET);
        int iY = oListOfOutputs.size()-iX;
        int iZ = iNumberOfMVPParts - oListOfOutputs.size();

        Set<String> oReturnOutput = null;
        Set<String> oSignaledFailure = new HashSet<String>(Arrays.asList(new String[]{mvpStructure.getSignaledFailure().getInvolvedFailure()}));

        if(iZ>iMaxTI){
            throw new RMPIReliabilitySimulationException(oSignaledFailure);
        } else if(iX>=iMinMS){
           oReturnOutput = RMPIInputModeler.RMPI_F0_AS_SET;
        } else if(iY<iMinMS){
            throw new RMPIReliabilitySimulationException(oSignaledFailure);
        } else {
            RMPIAgreementOfErrorsVectorWrapper oAgreementOfErrorsVectorWrapper = new RMPIAgreementOfErrorsVectorWrapper(mvpStructure.getAgreementOfErrorsVector(),iMinMS, iNumberOfMVPParts);
            if(Math.random() < oAgreementOfErrorsVectorWrapper.getAgreementOfErrors(iY)){
                Set<String> oSetOfOutputFailures = new HashSet<String>();
                String[] oOutputFailures = StringUtils.split(mvpStructure.getErroneousOutput().getInvolvedFailures(), RMPI_COMMA);
                oSetOfOutputFailures.addAll(Arrays.asList(oOutputFailures));

                oReturnOutput = oSetOfOutputFailures;
            }else{
                throw new RMPIReliabilitySimulationException(oSignaledFailure);
            }
        }

        return oReturnOutput;
    }

    /**
     * Run a simulation for an internal activity with an input.
     *
     * @param internalActivity the internal activity.
     * @param input            the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(InternalActivityType internalActivity, Set<String> input) throws RMPIReliabilitySimulationException {
        RMPIIAWrapper oIAWrapper = new RMPIIAWrapper(m_oInputModel, internalActivity.getFailureModel());

        double dTotalProbability = 0.0;
        double dRandomProbability = Math.random();
        for (Set<String> oSignaledFailure : m_oOrderedAFS) {
            dTotalProbability += oIAWrapper.getProbability(input, oSignaledFailure).doubleValue();
            if (dRandomProbability < dTotalProbability) {
                throw new RMPIReliabilitySimulationException(oSignaledFailure);
            }
        }

        for (Set<String> oOutput : m_oOrderedAIOS) {
            dTotalProbability += oIAWrapper.getProbability(input, oOutput).doubleValue();
            if (dRandomProbability < dTotalProbability) {
                return oOutput;
            }
        }

        //never reach
        return null;
    }

    /**
     * Run a simulation for a calling activity with an input.
     *
     * @param componentInstanceName the current component instance name.
     * @param callingActivity       a calling activity.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, CallingActivityType callingActivity, Set<String> input) throws RMPIReliabilitySimulationException {
        String strCalledService = callingActivity.getCalledService();

        ComponentConnectorType oComponentConnector = m_oInputModel.getComponentConnectorFor(componentInstanceName, strCalledService);
        String strFromComponentInstance = oComponentConnector.getFromService().getFromComponentInstance();
        String strFromProvidedServiceName = oComponentConnector.getFromService().getFromProvidedService();

        return runSimulation(strFromComponentInstance, strFromProvidedServiceName, input);
    }

    /**
     * Run a simulation for a branching structure with an input.
     *
     * @param componentInstanceName the current component instance name.
     * @param branchingStructure    a branching structure.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, BranchingStructureType branchingStructure, Set<String> input) throws RMPIReliabilitySimulationException {
        IfPart[] oIfParts = branchingStructure.getIfPartArray();

        double dTotalProbability = 0.0;
        double dRandomProbability = Math.random();
        for (int i = 0; i < oIfParts.length; i++) {
            dTotalProbability += m_oInputModel.getUsageProfilePartBCProbability(m_oUsageProfilePart, oIfParts[i].getBranchingCondition()).getValue();
            if (dRandomProbability < dTotalProbability) {
                return runSimulation(componentInstanceName, oIfParts[i], input);
            }

        }

        if(branchingStructure.getElsePart() != null){
            return runSimulation(componentInstanceName, branchingStructure.getElsePart(), input);
        } else {
            return input;
        }

    }

    /**
     * Run a simulation for a looping structure with an input.
     *
     * @param componentInstanceName the current component instance name.
     * @param loopingStructure      a looping structure.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, LoopingStructureType loopingStructure, Set<String> input) throws RMPIReliabilitySimulationException {
        UsageProfilePartType.LCAverage oLCAverage = m_oInputModel.getUsageProfilePartLCAverage(m_oUsageProfilePart, loopingStructure.getLoopCount());
        if(oLCAverage != null){
            Integer oLoopCountAverage = oLCAverage.getValue();

            if(loopingStructure.getLoopType() == LoopingStructureType.LoopType.SEQUENTIAL){
                for (int i = 0; i < oLoopCountAverage.intValue(); i++) {
                    input = runSimulation(componentInstanceName, loopingStructure.getLoopingPart(), input);
                }

                return input;
            }

            //parallel looping structure
            Set<String> oReturnSet = RMPIInputModeler.RMPI_F0_AS_SET;

            for (int i = 0; i < oLoopCountAverage.intValue(); i++) {
                Set<String> oOutput = runSimulation(componentInstanceName, loopingStructure.getLoopingPart(), input);
                if (!oOutput.equals(RMPIInputModeler.RMPI_F0_AS_SET)) {
                    oReturnSet = Sets.union(oReturnSet, oOutput);
                }
            }

            return oReturnSet;
        }

        RMPILCProbabilityDistributionWrapper oLCProbabilityDistributionWrapper = new RMPILCProbabilityDistributionWrapper(m_oInputModel.getUsageProfilePartLCProbabilityDistribution(m_oUsageProfilePart, loopingStructure.getLoopCount()));
        double dTotalProb = 0.0;
        double dRanProb = Math.random();
        Integer[] oLCValues = oLCProbabilityDistributionWrapper.getLCValues();
        for(int i=0; i< oLCValues.length ;i++){
            dTotalProb+= oLCProbabilityDistributionWrapper.getProbability(oLCValues[i]);
            if (dRanProb < dTotalProb) {

                if(loopingStructure.getLoopType() == LoopingStructureType.LoopType.SEQUENTIAL){
                    for (int j = 0; j < oLCValues[i]; j++) {
                        input = runSimulation(componentInstanceName, loopingStructure.getLoopingPart(), input);
                    }

                    return input;
                }

                //parallel looping structure
                Set<String> oReturnSet = RMPIInputModeler.RMPI_F0_AS_SET;

                for (int j = 0; j < oLCValues[i]; j++) {
                    Set<String> oOutput = runSimulation(componentInstanceName, loopingStructure.getLoopingPart(), input);
                    if (!oOutput.equals(RMPIInputModeler.RMPI_F0_AS_SET)) {
                        oReturnSet = Sets.union(oReturnSet, oOutput);
                    }
                }

                return oReturnSet;
            }
        }

        //never reach
        return null;
    }

   /**
     * Run a simulation for a sequential structure with an input.
     *
     * @param componentInstanceName the current component instance name.
     * @param sequentialStructure   a sequential structure.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, SequentialStructureType sequentialStructure, Set<String> input) throws RMPIReliabilitySimulationException {
        CallPropagationType[] oSequentialParts = sequentialStructure.getSequentialPartArray();

        for (int i = 0; i < oSequentialParts.length; i++) {
            input = runSimulation(componentInstanceName, oSequentialParts[i], input);
        }

        return input;
    }

    /**
     * Run a simulation for a parallel structure with an input.
     *
     * @param componentInstanceName the current component instance name.
     * @param parallelStructure     a parallel structure.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, ParallelStructureType parallelStructure, Set<String> input) throws RMPIReliabilitySimulationException {
        CallPropagationType[] oParallelParts = parallelStructure.getParallelPartArray();

        Set<String> oReturnSet = RMPIInputModeler.RMPI_F0_AS_SET;

        for (int i = 0; i < oParallelParts.length; i++) {
            Set<String> oOutput = runSimulation(componentInstanceName, oParallelParts[i], input);
            if (!oOutput.equals(RMPIInputModeler.RMPI_F0_AS_SET)) {
                oReturnSet = Sets.union(oReturnSet, oOutput);
            }
        }

        return oReturnSet;
    }

    /**
     * Run a simulation for a parallel structure with an input.
     *
     * @param componentInstanceName the current component instance name.
     * @param retryStructure        a retry structure.
     * @param input                 the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, RetryStructureType retryStructure, Set<String> input) throws RMPIReliabilitySimulationException {
        int iRetryCount = retryStructure.getRetryCount();
        Set<Set<String>> oFHSet = getFHSet(retryStructure.getHandledFailures());

        int iCounter = 0;
        while (true) {
            try {
                return runSimulation(componentInstanceName, retryStructure.getRetryPart(), input);
            } catch (RMPIReliabilitySimulationException oException) {
                if (!oFHSet.contains(oException.getSignaledFailure()) || ++iCounter > iRetryCount) {
                    throw oException;
                }
            }
        }
    }

    /**
     * Run a simulation for a parallel structure with an input.
     *
     * @param componentInstanceName  the current component instance name.
     * @param multiTryCatchStructure a multi try catch structure.
     * @param input                  the input.
     * @return the output.
     * @throws RMPIReliabilitySimulationException
     *          if there is a signaled failure.
     */
    private Set<String> runSimulation(String componentInstanceName, MultiTryCatchStructureType multiTryCatchStructure, Set<String> input) throws RMPIReliabilitySimulationException {
        MultiTryCatchPart[] oMultiTryCatchParts = multiTryCatchStructure.getMultiTryCatchPartArray();
        int iCurrentPartIndex = 0;
        while (true) {
            try {
                return runSimulation(componentInstanceName, oMultiTryCatchParts[iCurrentPartIndex], input);
            } catch (RMPIReliabilitySimulationException oException) {
                Set<String> oSignaledFailure = oException.getSignaledFailure();

                int x = iCurrentPartIndex + 1;
                for (; x < oMultiTryCatchParts.length; x++) {
                    Set<Set<String>> oFHSet = getFHSet(oMultiTryCatchParts[x].getHandledFailures());
                    if (oFHSet.contains(oSignaledFailure)) {
                        iCurrentPartIndex = x;
                    }
                }

                if (x == oMultiTryCatchParts.length) {
                    throw oException;
                }
            }
        }
    }

    /**
     * Get the FH set from handled failures of a fault tolerance structure.
     *
     * @param handledFailures handled failures.
     * @return the FH set.
     */
    private Set<Set<String>> getFHSet(HandledFailuresType handledFailures) {
        String strHandledFailures = handledFailures.getStringValue();
        if (RMPI_NONE.equals(strHandledFailures)) {
            return new HashSet<Set<String>>();
        }

        if (RMPI_ALL.equals(strHandledFailures)) {
            return m_oInputModel.getAFS();
        }

        Set<Set<String>> oFHSet = new HashSet<Set<String>>();
        String[] oHandledFailures = StringUtils.split(strHandledFailures, RMPI_COMMA);
        for (String strHandledFailure : oHandledFailures) {
            Set<String> oSet = new HashSet<String>();
            oSet.add(strHandledFailure);
            oFHSet.add(oSet);
        }

        return oFHSet;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Thread#run()
     */
    @Override
    public void run() {
        int iRunCounter = 0;
        while (true) {
            if (iRunCounter >= m_iNumberOfRuns) {
                break;
            }

            if (m_isStopped) {
                break;
            }

            try {
                Set<String> oOutput = runSimulation();
                updateSimulationStatistics(oOutput);
            } catch (RMPIReliabilitySimulationException oException) {
                updateSimulationStatistics(oException.getSignaledFailure());
            }

            informProgressForAllListeners(++iRunCounter);
        }

        informCompletionForAllListeners();
    }

    /**
     * Update the simulation statistics.
     *
     * @param oOutput the output.
     */
    private void updateSimulationStatistics(Set<String> oOutput) {
        m_oSimulationStatistics.increaseCounter(oOutput);
    }

    /**
     * Inform all the registered listeners that there is an progress.
     *
     * @param numberOfCompletedRuns the number of completed runs.
     */
    private void informProgressForAllListeners(int numberOfCompletedRuns) {
        for (RMPIReliabilitySimulationListener oListener : m_oListeners) {
            oListener.informProgress(numberOfCompletedRuns);
        }
    }

    /**
     * Inform all the registered listeners that there is a completion.
     */
    private void informCompletionForAllListeners() {
        for (RMPIReliabilitySimulationListener oListener : m_oListeners) {
            oListener.informCompletion(m_oSimulationStatistics);
        }
    }
}

